淺談 C# Property (屬性) 語法糖與 NRT 機制演進
TLDR
field關鍵字 (C# 14):允許在自動屬性中直接存取編譯器產生的隱藏欄位,無需退回手寫 Backing Field 即可加入邏輯。- 避免
=>與{ get; } =混淆:=>為動態計算,每次呼叫都會執行;{ get; } =為靜態快取,僅在物件初始化時執行一次。 - NRT 實用性改善:透過
init與required關鍵字,解決了 DTO 在 NRT 機制下被迫使用null!或無意義預設值的困擾。 - 強制編譯檢查:透過
<WarningsAsErrors>nullable</WarningsAsErrors>設定,可將 NRT 警告提升為編譯錯誤,確保團隊合約一致性。 - 建構子與
required的協作:使用[SetsRequiredMembers]屬性可告知編譯器建構子已完成必要屬性賦值,解決與required的衝突。
C# Property (屬性) 的語法演進史
C# 屬性語法經歷了多次演進,旨在簡化定義並減少冗餘代碼。
1. 早期古典寫法 (Backing Field)
在 C# 1.0 時期,屬性必須手動宣告私有欄位來儲存資料。
public class User {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}2. 自動實作屬性 (Auto-Implemented Properties)
C# 3.0 引入,由編譯器自動產生底層欄位,適用於無需額外邏輯的資料容器。
public class User {
public string Name { get; set; }
}3. 屬性初始值設定項 (Property Initializers)
C# 6.0 允許在自動屬性定義時直接給定初始值。
public class User {
public string Name { get; set; } = "Default Name";
}4. Expression-bodied 屬性
C# 6.0 與 7.0 引入 => 語法,使屬性定義更簡潔。
WARNING
什麼情況下會遇到問題:混淆 Expression-bodied (=>) 與 Property Initializer ({ get; } =) 的執行時機。
public string Name => "Default Name":每次讀取時重新計算。public string Name { get; } = "Default Name":僅在物件實體化時執行一次。
錯誤範例:
public class Order {
// 錯誤:每次讀取都會產生新 Guid,導致序列化或 Log 追蹤異常
public Guid OrderId => Guid.NewGuid();
// 正確:僅在 new() 時產生一次
public Guid CorrectOrderId { get; } = Guid.NewGuid();
}5. 半自動屬性與 field 關鍵字
什麼情況下會遇到問題:當自動屬性需要在 set 中加入微小邏輯(如 Trim() 或通知變更)時,過去必須退回手寫 Backing Field。
C# 14 引入 field 關鍵字,直接存取編譯器背後的欄位:
public class User {
public string Name {
get;
set => field = value.Trim();
}
}TIP
建議邏輯處理優先放在 set 中,避免 get 頻繁呼叫帶來的額外開銷,並減少 Entity Framework Core 等框架直接存取 Backing Field 時的潛在問題。
NRT (Nullable Reference Types) 與檢查機制的補全
NRT 旨在透過 ? 標註明確宣告參考型別是否可為空。若要強制執行,可在專案檔設定 <WarningsAsErrors>nullable</WarningsAsErrors>。
為什麼以前會想關掉?
在 C# 8.0 至 10.0 時期,DTO 屬性若非 Null,必須提供預設值或使用 null! 欺騙編譯器,這會導致類別定義承擔無法保證的合約。
機制的補全
透過以下語法,NRT 的開發體驗獲得顯著改善:
init(C# 9.0):確保屬性僅在初始化期間可賦值,維持不可變性。required(C# 11.0):強制呼叫端在new()時必須賦值,無需在類別內寫null!。
public class UserDto {
public required string UserName { get; init; }
}
// 呼叫端必須給值,否則編譯失敗
UserDto dto = new() { UserName = "Alice" };[SetsRequiredMembers]:解決與建構子的衝突
什麼情況下會遇到問題:當類別同時包含自訂建構子與 required 屬性時,編譯器會因無法透過 { } 初始化而發出警告。
using System.Diagnostics.CodeAnalysis;
public class User {
public required string UserName { get; init; }
[SetsRequiredMembers]
public User(string userName) {
UserName = userName;
}
}required 在 Web API 的應用
在 System.Text.Json 序列化中,若屬性標記為 required 但前端漏傳,系統會拋出 JsonException,這有助於區分「前端傳遞預設值」與「前端漏傳」的差異。
異動歷程
- 初版文件建立。